/*
 * $Id: server.c 530 2008-07-01 20:53:27Z jasta00 $
 *
 * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

#include "main.h"

#include <netinet/in.h>

#include <libsyncml/syncml.h>
#include <libsyncml/sml_auth.h>
#include <libsyncml/sml_ds_server.h>

#include <libsoup/soup-server.h>

#define SYNCML_SOUP_HACK
#include <libsyncml/http_server.h>

#include <libsyncml/sml_devinf_obj.h>

#include "db.h"
#include "meta.h"
#include "session.h"
#include "content.h"

/*****************************************************************************/

#define PORT 5545
#define DBNAME "music"
#define DBMIME "application/x-fivedb"

static GList *sessions = NULL;
static SmlManager *manager = NULL;
static SmlDevInfAgent *agent = NULL;
static SmlTransport *server = NULL;

/*****************************************************************************/

static void carp(const char *msg, SmlError *err)
{
	assert(err != NULL);

	if (msg == NULL)
		msg = "carp";

	printf("%s: %s\n", msg, smlErrorPrint(&err));
	smlErrorDeref(&err);
}

static void croak(const char *msg, SmlError *err)
{
	carp(msg, err);
	exit(EXIT_FAILURE);
}

static FiveSession *find_five_session(SmlSession *sess)
{
	GList *s;

	for (s = sessions; s != NULL; s = s->next)
	{
		FiveSession *fsess = s->data;

		if (smlDsSessionGetSession(fsess->dsession) == sess)
			return fsess;
	}

	return NULL;
}

static FiveSession *find_five_session_by_ds(SmlDsSession *dsess)
{
	GList *s;

	for (s = sessions; s != NULL; s = s->next)
	{
		FiveSession *fsess = s->data;

		if (fsess->dsession == dsess)
			return fsess;
	}

	return NULL;
}

static gboolean _sessions_prepare(GSource *src, gint *timeout)
{
	*timeout = 50;
	return FALSE;
}

static gboolean _sessions_check(GSource *src)
{
	GList *s;

	for (s = sessions; s != NULL; s = s->next)
	{
		FiveSession *fsess = s->data;

		if (smlDsSessionCheck(fsess->dsession) == TRUE)
			return TRUE;
	}

	if (smlManagerCheck(manager) == TRUE)
		return TRUE;

	return FALSE;
}

static gboolean _sessions_dispatch(GSource *src, GSourceFunc cb, gpointer udata)
{
	GList *s;

	for (s = sessions; s != NULL; s = s->next)
	{
		FiveSession *fsess = s->data;
		smlDsSessionDispatch(fsess->dsession);
	}

	smlManagerDispatch(manager);

	return TRUE;
}

static void _sessions_attach(void)
{
	GSourceFuncs *funcs = g_malloc0(sizeof(GSourceFuncs));
	funcs->prepare = _sessions_prepare;
	funcs->check = _sessions_check;
	funcs->dispatch = _sessions_dispatch;
	funcs->finalize = NULL;

	GSource *src = g_source_new(funcs, sizeof(GSource));
	g_source_set_callback(src, NULL, NULL, NULL);
	g_source_attach(src, NULL);
}

static void _manager_event(SmlManager *mgr, SmlManagerEventType type, SmlSession *sess, SmlError *err, gpointer udata)
{
	SmlError *locerr = NULL;

	switch (type)
	{
		case SML_MANAGER_SESSION_FLUSH:
		case SML_MANAGER_CONNECT_DONE:
		case SML_MANAGER_DISCONNECT_DONE:
			break;
		case SML_MANAGER_TRANSPORT_ERROR:
			printf("transport error: %s\n", smlErrorPrint(&err));
			break;
		case SML_MANAGER_SESSION_NEW:
			printf("new session: id=%s (uri=%s)\n",
			  smlSessionGetSessionID(sess),
			  smlLocationGetURI(smlSessionGetSource(sess)));
			smlSessionUseNumberOfChanges(sess, TRUE);
			smlSessionUseLargeObjects(sess, TRUE);
			smlSessionSetRequestedMaxObjSize(sess, 4096);

			break;
		case SML_MANAGER_SESSION_FINAL:
#if 0
			printf("session reported final: id=%s\n",
			  smlSessionGetSessionID(sess));
#endif
			smlSessionFlush(sess, TRUE, NULL);
			break;
		case SML_MANAGER_SESSION_END:
			{
				FiveSession *fsess;

				printf("end session: id=%s\n", smlSessionGetSessionID(sess));

				/* XXX: We only need this because libsyncml is designed weird
				 * and doesn't give us a way to access various layers of the
				 * server data structure. */
				if ((fsess = find_five_session(sess)) == NULL)
					g_warning("Not a Five session?  Gibberish on the wire?");
				else
				{
					if (fsess->errors == 0)
					{
						g_message("updating anchors in server db");

						if (five_session_update_anchors(fsess) == FALSE)
							g_critical("shit");
					}

					/* XXX: We definitely need to clean up `fsess'... */
				}

				/* XXX: Do we have to call smlSessionUnref()? */
			}
			break;
		case SML_MANAGER_SESSION_WARNING:
			printf("WARNING: %s\n", smlErrorPrint(&err));
			break;
		case SML_MANAGER_SESSION_ERROR:
			g_critical("session %s: %s\n", 
			  sess ? smlSessionGetSessionID(sess) : "UNKNOWN",
			  smlErrorPrint(&err));

			/* XXX: Will this evoke SML_MANAGER_SESSION_END?  Methinks not. */
			//smlSessionEnd(sess, &err);

			break;

		default:
			printf("Huh?\n");
			break;
	}
}

static void _recv_alert_reply(SmlSession *sess, SmlStatus *status,
  gpointer udata)
{
	/* Do nothing. */
}

static SmlBool _recv_alert(SmlDsSession *dsess, SmlAlertType type,
  const char *last, const char *next, gpointer udata)
{
	FiveSession *fsess = udata;
	const char *src;
	SmlError *err = NULL;
	time_t lastId;
	time_t nextId;

	if (last == NULL)
		last = "0";

	if (next == NULL)
		next = "0";

	printf("alert for remote db %s: type=%d, last=%s, next=%s\n",
	  smlDsSessionGetLocation(dsess), type, last, next);

	lastId = (time_t)g_ascii_strtoull(last, NULL, 10);
	nextId = (time_t)g_ascii_strtoull(next, NULL, 10);

	if (type == SML_ALERT_ONE_WAY_FROM_SERVER)
	{
		if (nextId <= lastId)
		{
			g_critical("next anchor should be greater than last anchor");
			fsess->errors++;

			return FALSE;
		}

		if (fsess->client.last != lastId)
		{
			g_critical("last anchor does not match with server");
			fsess->errors++;

			return FALSE;
		}
	}
	else if (type == SML_ALERT_REFRESH_FROM_SERVER_BY_SERVER)
	{
		/* Do nothing. */
	}
	else
	{
		/* TODO: How do we reject? */
		printf("we only support sync type=204 or 210\n");
		abort();
	}

	fsess->client.next = nextId;
	fsess->type = type;

	/* Hmm, should we send SML_ALERT_ONE_WAY_FROM_SERVER? */
	SmlBool ret = smlDsSessionSendAlert(dsess, SML_ALERT_TWO_WAY,
	  last, next, _recv_alert_reply, NULL, &err);

	if (ret == FALSE)
	{
		printf("unable to send <Alert>: %s\n", smlErrorPrint(&err));
		fsess->errors++;
	}

	return TRUE;
}

static void _recv_sync_reply(SmlSession *sess, SmlStatus *status,
  gpointer udata)
{
	FiveSession *fsess = udata;
	SmlErrorType type;

	type = smlStatusGetCode(status);

	if (type != 200)
	{
		printf("ERROR: Received unusual <Sync> reply: %d\n",
		  smlStatusGetCode(status));

		smlSessionEnd(sess, NULL);
	}
}

static void _change_add_reply(SmlDsSession *dsess, SmlStatus *status,
  const char *newuid, gpointer udata)
{
	FiveChange *chg = udata;
	sqlite_int64 luid;
	char *endptr;
	int code;

	if ((code = smlStatusGetCode(status)) != 201)
	{
		printf("received an unusual reply to our add: %i\n", code);
		find_five_session_by_ds(dsess)->errors++;

		goto cleanup;
	}

	luid = g_ascii_strtoll(newuid, &endptr, 10);

	if (luid == 0 && newuid == endptr)
		g_warning("Could not understand <Map> id: %s", newuid);
	else
	{
		FiveSession *fsess = find_five_session_by_ds(dsess);
		assert(fsess != NULL);

		five_session_item_map(fsess, chg->mime, chg->guid, luid);
	}

cleanup:
	five_change_free(chg);
}

static void _change_replace_or_delete_reply(SmlDsSession *dsess,
  SmlStatus *status, const char *newuid, gpointer udata)
{
	FiveChange *chg = udata;
	int code;

	if ((code = smlStatusGetCode(status)) != 200)
		printf("received an unusual reply to our change: %i\n", code);

	five_change_free(chg);
}

static void _send_changes(gpointer data, gpointer udata)
{
	FiveSession *fsess = udata;
	FiveChange *chg = data;
	SmlError *error = NULL;
	SmlDsSessionWriteCb cb;
	char id[20];
	SmlBool ret;

	switch (chg->type)
	{
		case SML_CHANGE_ADD:
			cb = _change_add_reply;
			g_snprintf(id, sizeof(id), "%lu", (unsigned long)chg->guid);
			break;
		default:
			cb = _change_replace_or_delete_reply;
			g_snprintf(id, sizeof(id), "%lu", (unsigned long)chg->luid);
			break;
	}

	ret = smlDsSessionQueueChange(fsess->dsession, chg->type, id,
	  (const char *)chg->data, (unsigned int)chg->datasz, chg->mime,
	  cb, chg, &error);

	if (ret == FALSE)
	{
		carp("unable to queue change", error);
		fsess->errors++;
	}
}

static void _recv_sync(SmlDsSession *dsess, unsigned int numchanges,
  gpointer udata)
{
	FiveSession *fsess = udata;
	GList *chg;
	unsigned int chglen = 0;

	printf("[NOTSUPPORTED] preparing to receive %d changes\n", numchanges);

#if 0
	/* XXX: Implement this correctly. */
	if (g_static_mutex_trylock(&crawling) == FALSE)
		printf("Eeep, sync is in progress, we need to abort but can't!\n");
	else
		g_static_mutex_unlock(&crawling);
#endif

	fsess->server.next = time(NULL);
	chg = five_session_get_changes(fsess, &chglen);

	smlDsSessionSendSync(dsess, chglen, _recv_sync_reply, fsess, NULL);
	g_list_foreach(chg, _send_changes, fsess);
	smlDsSessionCloseSync(dsess, NULL);

	/* XXX: This is incomplete and will only not leak memory on a correct
	 * response from the server.  We need to implement some memory management
	 * here to ensure that the session tracks the changes to be freed and nukes
	 * them on session closure. */
	if (chg != NULL)
		g_list_free(chg);
}

static SmlBool _recv_change(SmlDsSession *dsess, SmlChangeType type,
  const char *uid, char *data, unsigned int size, const char *mime,
  gpointer udata, SmlError **err)
{
	printf("[NOTSUPPORTED] received ");

	switch (type)
	{
		case SML_CHANGE_ADD:
			printf("<Add>");
			break;
		case SML_CHANGE_REPLACE:
			printf("<Update>");
			break;
		case SML_CHANGE_DELETE:
			printf("<Delete>");
			break;
		default:
			abort();
			break;
	}

	printf(" %s of size %d and type %s\n", uid, size, mime);
	printf("\t\tData: %s\n", data);

	g_free(data);

	return TRUE;
}

static void _ds_alert(SmlDsSession *dsess, gpointer udata)
{
	FiveSession *fsess = five_session_new(dsess);
	assert(fsess != NULL);

	smlDsSessionGetAlert(dsess, _recv_alert, fsess);
	smlDsSessionGetSync(dsess, _recv_sync, fsess);
	smlDsSessionGetChanges(dsess, _recv_change, fsess);

	smlDsSessionRef(dsess);

	sessions = g_list_append(sessions, fsess);
}

void server_init(in_port_t port)
{
	SoupServer *soup;
	SmlTransportHttpServerConfig cfg;
	SmlAuthenticator *auth;
	SmlLocation *loc;
	SmlDsServer *dsserver;
	SmlDevInf *devinf;
	SmlDevInfDevTyp devtype = SML_DEVINF_DEVTYPE_SERVER;
	SmlError *error = NULL;

	memset(&cfg, 0, sizeof(cfg));
	cfg.port = port;

	if ((server = smlTransportNew(SML_TRANSPORT_HTTP_SERVER, &error)) == NULL)
		croak(NULL, error);

	if ((manager = smlManagerNew(server, &error)) == NULL)
		croak(NULL, error);

	smlManagerSetEventCallback(manager, _manager_event, NULL);

	if ((auth = smlAuthNew(&error)) == NULL)
		croak(NULL, error);

	smlAuthSetEnable(auth, FALSE);

	if (smlAuthRegister(auth, manager, &error) == FALSE)
		croak(NULL, error);

	if ((devinf = smlDevInfNew("five-server", devtype, &error)) == NULL)
		croak(NULL, error);

	if ((agent = smlDevInfAgentNew(devinf, &error)) == NULL)
		croak(NULL, error);

	if (smlDevInfAgentRegister(agent, manager, &error) == FALSE)
		croak(NULL, error);

	if ((loc = smlLocationNew(DBNAME, NULL, &error)) == NULL)
		croak(NULL, error);

	if ((dsserver = smlDsServerNew(DBMIME, loc, &error)) == NULL)
		croak(NULL, error);

	if (smlDsServerRegister(dsserver, manager, &error) == FALSE)
		croak(NULL, error);

	smlDsServerSetConnectCallback(dsserver, _ds_alert, NULL);

	if (smlTransportInitialize(server, &cfg, &error) == FALSE)
		croak(NULL, error);

	/* libsyncml hack to provide a handler for GET requests for the content
	 * provided by this server. */
	soup = smlTransportHttpServerGetSoup(server);
	assert(soup != NULL);

	soup_server_add_handler(soup, "/content", NULL,
	  content_serve_request, NULL, NULL);
	soup_server_add_handler(soup, "/meta", NULL,
	  meta_serve_request, NULL, NULL);

	if (smlManagerStart(manager, &error) == FALSE)
		croak(NULL, error);
}

void server_attach(void)
{
	_sessions_attach();
}

void server_finish(void)
{
	SmlError *error = NULL;

	smlManagerStop(manager);

	if (smlTransportFinalize(server, &error) == FALSE)
		carp(NULL, error);

	smlTransportFree(server);
}

